using System; 
using System.Collections.Generic;
using System.Data.Linq;

namespace System.Data.Linq.SqlClient { 

    // Resolves references to columns/expressions defined in other scopes 
    internal class SqlResolver { 
        Visitor visitor;
 
        internal SqlResolver() {
            this.visitor = new Visitor();
        }
 
        internal SqlNode Resolve(SqlNode node) {
            return this.visitor.Visit(node); 
        } 

        private static string GetColumnName(SqlColumn c) { 
#if DEBUG
            return c.Text;
#else
            return c.Name; 
#endif
        } 
 
        class Visitor : SqlScopedVisitor {
            SqlBubbler bubbler; 

            internal Visitor() {
                this.bubbler = new SqlBubbler();
            } 

            internal override SqlExpression VisitColumnRef(SqlColumnRef cref) { 
                SqlColumnRef result = this.BubbleUp(cref); 
                if (result == null) {
                    throw Error.ColumnReferencedIsNotInScope(GetColumnName(cref.Column)); 
                }
                return result;
            }
 
            private SqlColumnRef BubbleUp(SqlColumnRef cref) {
                for (Scope s = this.CurrentScope; s != null; s = s.ContainingScope) { 
                    if (s.Source != null) { 
                        SqlColumn found = this.bubbler.BubbleUp(cref.Column, s.Source);
                        if (found != null) { 
                            if (found != cref.Column)
                                return new SqlColumnRef(found);
                            return cref;
                        } 
                    }
                } 
                return null; 
            }
        } 

        internal class SqlScopedVisitor : SqlVisitor {
            internal Scope CurrentScope;
 
            internal class Scope {
                SqlNode source; 
                Scope containing; 
                internal Scope(SqlNode source, Scope containing) {
                    this.source = source; 
                    this.containing = containing;
                }
                internal SqlNode Source {
                    get { return this.source; } 
                }
                internal Scope ContainingScope { 
                    get { return this.containing; } 
                }
            } 

            internal SqlScopedVisitor() {
                this.CurrentScope = new Scope(null, null);
            } 

            internal override SqlExpression VisitSubSelect(SqlSubSelect ss) { 
                Scope save = this.CurrentScope; 
                this.CurrentScope = new Scope(null, this.CurrentScope);
                base.VisitSubSelect(ss); 
                this.CurrentScope = save;
                return ss;
            }
 
            internal override SqlSelect VisitSelect(SqlSelect select) {
                select.From = (SqlSource)this.Visit(select.From); 
 
                Scope save = this.CurrentScope;
                this.CurrentScope = new Scope(select.From, this.CurrentScope.ContainingScope); 

                select.Where = this.VisitExpression(select.Where);
                for (int i = 0, n = select.GroupBy.Count; i < n; i++) {
                    select.GroupBy[i] = this.VisitExpression(select.GroupBy[i]); 
                }
                select.Having = this.VisitExpression(select.Having); 
                for (int i = 0, n = select.OrderBy.Count; i < n; i++) { 
                    select.OrderBy[i].Expression = this.VisitExpression(select.OrderBy[i].Expression);
                } 
                select.Top = this.VisitExpression(select.Top);
                select.Row = (SqlRow)this.Visit(select.Row);

                // selection must be able to see its own projection 
                this.CurrentScope = new Scope(select, this.CurrentScope.ContainingScope);
                select.Selection = this.VisitExpression(select.Selection); 
 
                this.CurrentScope = save;
                return select; 
            }

            internal override SqlStatement VisitInsert(SqlInsert sin) {
                Scope save = this.CurrentScope; 
                this.CurrentScope = new Scope(sin, this.CurrentScope.ContainingScope);
                base.VisitInsert(sin); 
                this.CurrentScope = save; 
                return sin;
            } 

            internal override SqlStatement VisitUpdate(SqlUpdate sup) {
                Scope save = this.CurrentScope;
                this.CurrentScope = new Scope(sup.Select, this.CurrentScope.ContainingScope); 
                base.VisitUpdate(sup);
                this.CurrentScope = save; 
                return sup; 
            }
 
            internal override SqlStatement VisitDelete(SqlDelete sd) {
                Scope save = this.CurrentScope;
                this.CurrentScope = new Scope(sd, this.CurrentScope.ContainingScope);
                base.VisitDelete(sd); 
                this.CurrentScope = save;
                return sd; 
            } 

            internal override SqlSource VisitJoin(SqlJoin join) { 
                Scope save = this.CurrentScope;
                switch (join.JoinType) {
                    case SqlJoinType.CrossApply:
                    case SqlJoinType.OuterApply: { 
                        this.Visit(join.Left);
                        Scope tmp = new Scope(join.Left, this.CurrentScope.ContainingScope); 
                        this.CurrentScope = new Scope(null, tmp); 
                        this.Visit(join.Right);
                        Scope tmp2 = new Scope(join.Right, tmp); 
                        this.CurrentScope = new Scope(null, tmp2);
                        this.Visit(join.Condition);
                        break;
                    } 
                    default: {
                        this.Visit(join.Left); 
                        this.Visit(join.Right); 
                        this.CurrentScope = new Scope(null, new Scope(join.Right, new Scope(join.Left, this.CurrentScope.ContainingScope)));
                        this.Visit(join.Condition); 
                        break;
                    }
                }
                this.CurrentScope = save; 
                return join;
            } 
        } 

        // finds location of expression definition and re-projects that value all the 
        // way to the outermost projection
        internal class SqlBubbler : SqlVisitor {
            SqlColumn match;
            SqlColumn found; 

            internal SqlBubbler() { 
            } 

            internal SqlColumn BubbleUp(SqlColumn col, SqlNode source) { 
                this.match = this.GetOriginatingColumn(col);
                this.found = null;
                this.Visit(source);
                return this.found; 
            }
 
            internal SqlColumn GetOriginatingColumn(SqlColumn col) { 
                SqlColumnRef cref = col.Expression as SqlColumnRef;
                if (cref != null) { 
                    return this.GetOriginatingColumn(cref.Column);
                }
                return col;
            } 

            internal override SqlRow VisitRow(SqlRow row) { 
                foreach (SqlColumn c in row.Columns) { 
                    if (this.RefersToColumn(c, this.match)) {
                        if (this.found != null) { 
                            throw Error.ColumnIsDefinedInMultiplePlaces(GetColumnName(this.match));
                        }
                        this.found = c;
                        break; 
                    }
                } 
                return row; 
            }
 
            internal override SqlTable VisitTable(SqlTable tab) {
                foreach (SqlColumn c in tab.Columns) {
                    if (c == this.match) {
                        if (this.found != null) 
                            throw Error.ColumnIsDefinedInMultiplePlaces(GetColumnName(this.match));
                        this.found = c; 
                        break; 
                    }
                } 
                return tab;
            }

            internal override SqlSource VisitJoin(SqlJoin join) { 
                switch (join.JoinType) {
                    case SqlJoinType.CrossApply: 
                    case SqlJoinType.OuterApply: { 
                        this.Visit(join.Left);
                        if (this.found == null) { 
                            this.Visit(join.Right);
                        }
                        break;
                    } 
                    default: {
                        this.Visit(join.Left); 
                        this.Visit(join.Right); 
                        break;
                    } 
                }
                return join;
            }
 
            internal override SqlExpression VisitTableValuedFunctionCall(SqlTableValuedFunctionCall fc) {
                foreach (SqlColumn c in fc.Columns) { 
                    if (c == this.match) { 
                        if (this.found != null)
                            throw Error.ColumnIsDefinedInMultiplePlaces(GetColumnName(this.match)); 
                        this.found = c;
                        break;
                    }
                } 
                return fc;
            } 
 
            private void ForceLocal(SqlRow row, string name) {
                bool isLocal = false; 
                // check to see if it already exists locally
                foreach (SqlColumn c in row.Columns) {
                    if (this.RefersToColumn(c, this.found)) {
                        this.found = c; 
                        isLocal = true;
                        break; 
                    } 
                }
                if (!isLocal) { 
                    // need to put this in the local projection list to bubble it up
                    SqlColumn c = new SqlColumn(found.ClrType, found.SqlType, name, this.found.MetaMember, new SqlColumnRef(this.found), row.SourceExpression);
                    row.Columns.Add(c);
                    this.found = c; 
                }
            } 
 
            private bool IsFoundInGroup(SqlSelect select) {
                // does the column happen to be listed in the group-by clause? 
                foreach (SqlExpression exp in select.GroupBy) {
                    if (this.RefersToColumn(exp, this.found) || this.RefersToColumn(exp, this.match)) {
                        return true;
                    } 
                }
                return false; 
            } 

            internal override SqlSelect VisitSelect(SqlSelect select) { 
                // look in this projection
                this.Visit(select.Row);

                if (this.found == null) { 
                    // look in upstream projections
                    this.Visit(select.From); 
 
                    // bubble it up
                    if (this.found != null) { 
                        if (select.IsDistinct && !match.IsConstantColumn) {
                            throw Error.ColumnIsNotAccessibleThroughDistinct(GetColumnName(this.match));
                        }
                        if (select.GroupBy.Count == 0 || this.IsFoundInGroup(select)) { 
                            this.ForceLocal(select.Row, this.found.Name);
                        } 
                        else { 
                            // found it, but its hidden behind the group-by
                            throw Error.ColumnIsNotAccessibleThroughGroupBy(GetColumnName(this.match)); 
                        }
                    }
                }
 
                return select;
            } 
        } 
    }
} 

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.